/*
 * Copyright 2007-2008 Andrew O'Malley
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


package ws.quokka.plugin.maven;

import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Checksum;

import ws.quokka.core.bootstrap_util.Assert;
import ws.quokka.core.bootstrap_util.IOUtils;
import ws.quokka.core.bootstrap_util.Logger;
import ws.quokka.core.bootstrap_util.PropertiesUtil;
import ws.quokka.core.bootstrap_util.Reflect;
import ws.quokka.core.plugin_spi.support.AntUtils;
import ws.quokka.core.plugin_spi.support.TypedProperties;
import ws.quokka.core.repo_resolver.ResolvedPath;
import ws.quokka.core.repo_resolver.Resolver;
import ws.quokka.core.repo_spi.RepoArtifact;
import ws.quokka.core.repo_spi.RepoArtifactId;
import ws.quokka.core.repo_spi.RepoConflict;
import ws.quokka.core.repo_spi.RepoDependency;
import ws.quokka.core.repo_spi.RepoOverride;
import ws.quokka.core.repo_spi.RepoPath;
import ws.quokka.core.repo_spi.RepoPathSpec;
import ws.quokka.core.repo_spi.Repository;
import ws.quokka.core.repo_spi.UnresolvedArtifactException;
import ws.quokka.core.util.Strings;
import ws.quokka.core.version.Version;

import java.io.File;

import java.net.URL;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;


/**
 *
 */
public class Verifier {
    //~ Instance fields ------------------------------------------------------------------------------------------------

    private Repository repository;
    private Resolver resolver;
    private Logger log;
    private Project project;
    private Map mavenIds = new HashMap();
    private List errors = new ArrayList();
    private List warnings = new ArrayList();
    private List verifiedIssues;
    private Map existingVersions = new HashMap(); // <unversioned RepoArtifactId, TreeSet<Version>>
    private TypedProperties properties;
    private Set optionalLibs = new HashSet();
    private Map artifacts = new HashMap();
    private Set parentIds = new HashSet();
    private boolean failOnWarnings;
    private boolean compareMavenHash;
    private boolean compareMavenPaths;
    private MavenImporter importer;
    private List commonRoots;

    //~ Constructors ---------------------------------------------------------------------------------------------------

    public Verifier(Repository repository, Resolver resolver, Logger log, TypedProperties properties, Project project,
        List verifiedIssues, MavenImporter importer) {
        this.repository = repository;
        this.resolver = resolver;
        this.log = log;
        this.properties = properties;
        this.project = project;
        this.verifiedIssues = verifiedIssues;
        this.importer = importer;
        this.failOnWarnings = properties.getBoolean("failOnWarnings", true);
        this.compareMavenHash = properties.getBoolean("compareMavenHash", false);
        this.compareMavenPaths = properties.getBoolean("compareMavenPaths", false);
        optionalLibs.add(new RepoArtifactId("apache.xerces", null, "jar", (Version)null).mergeDefaults());
        optionalLibs.add(new RepoArtifactId("apache.xml-commons.xml-apis", null, "jar", (Version)null).mergeDefaults());
        optionalLibs.add(new RepoArtifactId("apache.xalan", null, "jar", (Version)null).mergeDefaults());
        this.commonRoots = Strings.commaSepList(properties.getString("commonRoots", null));
    }

    public Verifier() {
    }

    //~ Methods --------------------------------------------------------------------------------------------------------

    public void verify(Collection idsToVerify) {
        // TODO: Verify that no optional dependencies will conflict
        if (!compareMavenPaths) {
            log.info("Comparison of resolved paths to maven paths is disabled due to the performance overhead.\n"
                + "    It can be enabled with -DcompareMavenPaths=true.");
        }

        if (!compareMavenHash) {
            log.info("Comparison of hashes to maven hashes is disabled due to the performance overhead.\n"
                + "    It can be enabled with -DcompareMavenHash=true.");
        }

        // Create an index of available versions in this repository and parents
        log.info("Creating index of artifacts ...");

        Collection ids = repository.listArtifactIds(true);

        for (Iterator i = ids.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();
            RepoArtifactId unversioned = toUnversioned(id);
            Set versions = (Set)this.existingVersions.get(unversioned);

            if (versions == null) {
                versions = new TreeSet();
                this.existingVersions.put(unversioned, versions);
            }

            versions.add(id.getVersion());

            RepoArtifact artifact = repository.resolve(id, false);
            artifacts.put(id, artifact);

            // Generate the hashes if necessary
            if ((artifact.getHash() == null) && (artifact.getLocalCopy() != null)) {
                artifact.setHash(hash(artifact.getLocalCopy()).getHash());
            }

            // Collect the maven ids
            MavenId mavenId = getMavenId(artifact);
            Set mIds = (Set)mavenIds.get(mavenId);

            if (mIds == null) {
                mIds = new TreeSet(); // Order important error is reported in the same order each time
                mavenIds.put(mavenId, mIds);
            }

            mIds.add(artifact.getId().toShortString());
        }

        loadParentIds();

        // Only verify the ids in this repository
        log.info("Verifying repository '" + repository.getName() + "'");
        idsToVerify = (idsToVerify.size() != 0) ? idsToVerify : repository.listArtifactIds(false);

        for (Iterator i = idsToVerify.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();
            verify(id);
        }

        verifyPossibleGroups(idsToVerify);

        Assert.isTrue((errors.size() == 0) && (!failOnWarnings || (warnings.size() == 0)),
            "The repository has issues. Check the log output above for details");
    }

    protected void verifyPossibleGroups(Collection ids) {
        Map commonParents = new HashMap();

        for (Iterator i = ids.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();

            if (id.getName().equals(RepoArtifactId.defaultName(id.getGroup())) && !isLicense(id)) {
                String group = id.getGroup();

                // Strip off any common root as projects under a common root are not necessarily related
                for (Iterator j = commonRoots.iterator(); j.hasNext();) {
                    String root = (String)j.next();

                    if (group.startsWith(root)) {
                        group = group.substring(root.length());

                        break;
                    }
                }

                // Drop the last subgroup
                int index = group.lastIndexOf(".");

                if (index == -1) {
                    continue; // No ancestor ... this group is unique
                }

                group = group.substring(0, index);

                Set possible = (Set)commonParents.get(group);

                if (possible == null) {
                    possible = new TreeSet();
                    commonParents.put(group, possible);
                }

                possible.add(id);
            }
        }

        for (Iterator i = commonParents.entrySet().iterator(); i.hasNext();) {
            Map.Entry entry = (Map.Entry)i.next();

//            String key = (String) entry.getKey();
            Set possible = (Set)entry.getValue();

            if (possible.size() < 2) {
                continue;
            }

//            System.out.println(key + " -> " + idsToString(possible));
            Version last = null;
            boolean match = true;

            for (Iterator j = possible.iterator(); j.hasNext();) {
                RepoArtifactId id = (RepoArtifactId)j.next();
                match = (last == null) || last.equals(id.getVersion());
                last = id.getVersion();

                if (!match) {
                    break;
                }
            }

            if (match) {
                warn("Related groups refer to artifacts of same version: " + idsToString(possible));
            }
        }
    }

    private void loadParentIds() {
        String parents = properties.getString("parents", null);

        if (parents == null) {
            parents = repository.getFactory().getProperties().getProperty("q.repo." + repository.getName() + ".parents");

            if (parents == null) {
                log.warn(
                    "Cannot locate parent repository ids. If this repository has parents, specify them with -Dparents=<comma separated ids>");
            }
        }

        for (Iterator i = Strings.commaSepList(parents).iterator(); i.hasNext();) {
            String parent = (String)i.next();

            if (!Strings.isEmpty(parent)) {
                Repository parentRepository = repository.getFactory().getOrCreate(parent, true);
                parentIds.addAll(parentRepository.listArtifactIds(true));
            }
        }
    }

    private RepoArtifactId toUnversioned(RepoArtifactId id) {
        return new RepoArtifactId(id.getGroup(), id.getName(), id.getType(), (Version)null);
    }

    private boolean isFiltered(String issue) {
        issue = issue.replace('\n', ' ').replace('\r', ' '); // Line separators break matches

        for (Iterator j = verifiedIssues.iterator(); j.hasNext();) {
            String pattern = (String)j.next();

            if (issue.matches(pattern)) {
                log.verbose("Ingoring issue '" + issue + "' as it matches pattern '" + pattern + "'");

                return true;
            }
        }

        return false;
    }

    private void verify(RepoArtifactId id) {
        log.verbose("Verifying " + id.toShortString());

        try {
            verify(repository.resolve(id, false));
        } catch (UnresolvedArtifactException e) {
            error("Unresolved artifact: " + id.toShortString());
        }
    }

    private void error(String message) {
        if (!isFiltered(message)) {
            errors.add(message);
            log.error(message);
        }
    }

    private void warn(String message) {
        if (!isFiltered(message)) {
            warnings.add(message);
            log.warn(message);
        }
    }

    private boolean isLicense(RepoArtifactId id) {
        return id.getType().equals("license");
    }

    private void verify(RepoArtifact artifact) {
        RepoArtifactId id = artifact.getId();
        verifyLicenses(artifact);
        verifyStandardVersion(id);
        verifyNameMatchesGroup(id);
        verifyDescription(artifact);
        verifyChecksum(artifact);
        verifyOptionalLibs(artifact);
        verifyStub(artifact);
        verifyNotInParents(id);
        verifyConflictAndOverrideIds(artifact);

        // Check details against a previous version
        RepoArtifact previous = getPrevious(artifact);

        if (previous != null) {
            verifyPrevious(artifact, previous);
        }

        MavenId mavenId = getMavenId(artifact);
        verifyMavenId(artifact, mavenId);
        verifyResolvedPaths(artifact, previous, mavenId);
    }

    private void verifyConflictAndOverrideIds(RepoArtifact artifact) {
        // Make sure the conflict ids are valid
        Set conflictIds = new TreeSet();

        for (Iterator i = artifact.getConflicts().iterator(); i.hasNext();) {
            RepoConflict conflict = (RepoConflict)i.next();
            conflictIds.add(conflict.getId());
        }

        // Make sure the overrides exist
        Set overrideIds = new TreeSet();

        for (Iterator i = artifact.getOverrides().iterator(); i.hasNext();) {
            RepoOverride override = (RepoOverride)i.next();
            overrideIds.add(new RepoArtifactId(override.getGroup(), override.getName(), override.getType(),
                    override.getWithVersion()));
        }

        for (Iterator i = artifacts.keySet().iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();

            for (Iterator j = conflictIds.iterator(); j.hasNext();) {
                RepoArtifactId conflictId = (RepoArtifactId)j.next();

                if (id.matches(conflictId)) {
                    j.remove();
                }
            }

            for (Iterator j = overrideIds.iterator(); j.hasNext();) {
                RepoArtifactId overrideId = (RepoArtifactId)j.next();

                if (id.matches(overrideId)) {
                    j.remove();
                }
            }
        }

        if (overrideIds.size() != 0) {
            error("Overrides do not exist: " + artifact.getId().toShortString() + " -> " + idsToString(overrideIds));
        }

        if (conflictIds.size() != 0) {
            error("Conflicts do not exist: " + artifact.getId().toShortString() + " -> " + idsToString(conflictIds));
        }
    }

    String idsToString(Collection ids) {
        StringBuffer sb = new StringBuffer();

        for (Iterator i = ids.iterator(); i.hasNext();) {
            RepoArtifactId id = (RepoArtifactId)i.next();
            sb.append(id.toShortString());

            if (i.hasNext()) {
                sb.append(", ");
            }
        }

        return sb.toString();
    }

    private void verifyNotInParents(RepoArtifactId id) {
        if (parentIds.contains(id)) {
            error("Exists in parents: " + id.toShortString());
        }
    }

    private void verifyResolvedPaths(RepoArtifact artifact, RepoArtifact previous, MavenId mavenId) {
        RepoArtifactId id = artifact.getId();

        // Try resolving the path
        for (Iterator i = artifact.getPaths().iterator(); i.hasNext();) {
            RepoPath path = (RepoPath)i.next();

            try {
                verifyPath(artifact, previous, mavenId, path);
            } catch (Exception e) {
                error("Path unresolvable: " + path.getId() + " for artifact " + id.toShortString() + ": "
                    + e.getMessage());
            }
        }
    }

    private void verifyPath(RepoArtifact artifact, RepoArtifact previous, MavenId mavenId, RepoPath path) {
        RepoArtifactId id = artifact.getId();

        // Make sure all dependencies are in the repository
        ResolvedPath resolvedPath = resolver.resolvePath(path.getId(), artifact, new ArrayList(), true, false);

        // Check dependencies against previous version
        if ((previous != null) && (previous.getPath(path.getId()) != null)) {
            ResolvedPath previousPath = resolver.resolvePath(path.getId(), previous, new ArrayList(), true, false);
            String message = "Resolved path does not match previous: " + id.toShortString() + " " + path.getId();
            List artifacts = new ArrayList(resolvedPath.getArtifacts());

            if (previousPath.getArtifacts().size() != artifacts.size()) {
                warn(message);
            } else {
                for (Iterator j = artifacts.iterator(); j.hasNext();) {
                    RepoArtifact pathArtifact = (RepoArtifact)j.next();
                    RepoArtifactId unversioned = toUnversioned(pathArtifact.getId());

                    for (Iterator k = previousPath.getArtifacts().iterator(); k.hasNext();) {
                        RepoArtifact previousArtifact = (RepoArtifact)k.next();

                        if (toUnversioned(previousArtifact.getId()).equals(unversioned)) {
                            j.remove();

                            break;
                        }
                    }
                }

                if (artifacts.size() != 0) {
                    warn(message);
                }
            }
        }

        if (compareMavenPaths && (mavenId != null) && path.getId().equals("runtime")) {
            ResolvedMavenArtifact mavenArtifact = null;

            try {
//                System.out.println("Comparing maven path: " + id.toShortString());
                mavenArtifact = importer.resolveMavenArtifact(mavenId);
            } catch (Exception e) {
                error("Could not resolve maven artifact: " + id.toShortString());

                if (log.isVerboseEnabled()) {
                    log.verbose(e.getMessage());
                }
            }

            String message = importer.compare(mavenArtifact, artifact, resolvedPath);

            if (message != null) {
                warn("Maven and quokka runtime paths do not match: " + id.toShortString() + " -> " + message);
            }
        }
    }

    private void verifyMavenId(RepoArtifact artifact, MavenId mavenId) {
        // Additional checks if imported from maven
        RepoArtifactId id = artifact.getId();

        if (mavenId == null) {
            if (!isLicense(id)) {
                warn("Not in maven2 repository: " + id.toShortString());
            }

            return;
        }

        // Check for duplicates based on maven id
        verifyUniqueMavenId(mavenId);

        // Compare hash to maven repository
        if (compareMavenHash && !artifact.isStub()) {
            HashResult mavenHash = mavenHash(mavenId, properties, project);

            if (((mavenHash != null) && (artifact.getHash() != null))
                    && !artifact.getHash().toLowerCase(Locale.US).equals(mavenHash.getHash().toLowerCase(Locale.US))) {
                error("Maven hash mismatch: " + id.toShortString() + " - compared " + artifact.getLocalCopy()
                    + " with " + mavenHash.getSource());
            }
        }
    }

    private void verifyStub(RepoArtifact artifact) {
        RepoArtifactId id = artifact.getId();

        // Check for stubs don't have an artifact with them
        if (artifact.isStub() && (artifact.getLocalCopy() != null)) {
            error("Stub has artifact: " + id.toShortString());
        }
    }

    private void verifyOptionalLibs(RepoArtifact artifact) {
        // Check there are no mandatory dependencies on XML libs as they
        // are always optional for some JDKs now
        // - including: xerces, xalan, xml-apis
        RepoArtifactId id = artifact.getId();

        for (Iterator i = artifact.getDependencies().iterator(); i.hasNext();) {
            RepoDependency dependency = (RepoDependency)i.next();

            for (Iterator j = optionalLibs.iterator(); j.hasNext();) {
                RepoArtifactId xmlId = (RepoArtifactId)j.next();

                if (dependency.getId().matches(xmlId)) {
                    for (Iterator k = dependency.getPathSpecs().iterator(); k.hasNext();) {
                        RepoPathSpec pathSpec = (RepoPathSpec)k.next();

                        if (Boolean.TRUE.equals(pathSpec.isMandatory())) {
                            warn("Mandatory dependency on optional library: " + editHint(id));

                            return;
                        }
                    }
                }
            }
        }
    }

    private void verifyPrevious(RepoArtifact artifact, RepoArtifact previous) {
        RepoArtifactId id = artifact.getId();

        // Check the description matches
        if (!nsEquals(artifact.getDescription(), previous.getDescription()) && !isLicense(id)) {
            warn("Description does not equal previous: " + editHint(id));
        }

        // Check the dependencies match (this checks optional dependencies as well)
        boolean match = artifact.getDependencies().size() == previous.getDependencies().size();

        for (Iterator i = artifact.getDependencies().iterator(); match && i.hasNext();) {
            RepoDependency dependency = (RepoDependency)i.next();
            RepoDependency matching = findMatchingDependency(previous, dependency);

            if (matching == null) {
                match = false;

                break;
            }

            for (Iterator j = dependency.getPathSpecs().iterator(); j.hasNext();) {
                RepoPathSpec pathSpec = (RepoPathSpec)j.next();

                if (!findMatchingPathSpec(matching.getPathSpecs(), pathSpec)) {
                    match = false;

                    break;
                }
            }
        }

        if (!match) {
            warn("Dependencies and path specs do not match previous: " + editHint(id));
        }

        // Check the licenses match
        String message = "Licenses do not match previous: " + editHint(id);

        if (artifact.getLicenses().size() == previous.getLicenses().size()) {
            Set previousLicenses = new HashSet();

            for (Iterator i = previous.getLicenses().iterator(); i.hasNext();) {
                RepoArtifactId previousLicenseId = (RepoArtifactId)i.next();
                previousLicenses.add(toUnversioned(previousLicenseId));
            }

            for (Iterator i = artifact.getLicenses().iterator(); i.hasNext();) {
                RepoArtifactId license = (RepoArtifactId)i.next();

                if (!previousLicenses.contains(toUnversioned(license))) {
                    warn(message);
                }
            }
        } else {
            warn(message);
        }

        // TODO: Check overrides? Probably not important as unversioned paths are compared later ...
        if (artifact.isStub() != previous.isStub()) {
            warn("Stub does not match previous: " + editHint(id));
        }

        if (!artifact.getConflicts().equals(previous.getConflicts())) {
            warn("Conflicts do not match previous: " + editHint(id));
        }
    }

    private RepoArtifact getPrevious(RepoArtifact artifact) {
        RepoArtifactId id = artifact.getId();

        // Check this version doesn't exist in the parents
        Version previousVersion = null;
        Set versions = (Set)existingVersions.get(toUnversioned(id));

        for (Iterator i = versions.iterator(); i.hasNext();) {
            Version version = (Version)i.next();

            // Capture the previous version
            if (version.compareTo(id.getVersion()) < 0) {
                previousVersion = version;
            }
        }

        if (previousVersion == null) {
            return null;
        }

        RepoArtifact previous = (RepoArtifact)artifacts.get(previousVersion);

        if (previous != null) {
            return previous;
        }

        return repository.resolve(new RepoArtifactId(id.getGroup(), id.getName(), id.getType(), previousVersion), false);
    }

    private MavenId getMavenId(RepoArtifact artifact) {
        Properties importedFrom = new Properties();
        String[] tokens = Strings.trim(Strings.split(artifact.getImportedFrom(), ";"));

        for (int i = 0; (tokens != null) && (i < tokens.length); i++) {
            String token = tokens[i];
            String[] keyValue = Strings.trim(Strings.split(token, "="));

            if (keyValue.length == 2) {
                importedFrom.put(keyValue[0], keyValue[1]);
            }
        }

        MavenId mavenId = null;

        if (importedFrom.containsKey("maven2")) {
            mavenId = MavenId.parse(importedFrom.getProperty("maven2"));
        }

        return mavenId;
    }

    private void verifyChecksum(RepoArtifact artifact) {
        if ((artifact.getHash() == null) && (artifact.getLocalCopy() != null)) {
            artifact.setHash(hash(artifact.getLocalCopy()).getHash());
        }

        // Check for duplicates based on hashes
        if (artifact.getHash() != null) {
            verifyChecksum(artifact, artifact.getHash());

            // Check that there are no stub.jar's left lying around
            if (artifact.getHash().equals("129b509c1a1ac897cc0bf60bee1b5a4b")) {
                error("Stub has not been removed: " + artifact.getId().toShortString());
            }
        }
    }

    private void verifyDescription(RepoArtifact artifact) {
        RepoArtifactId id = artifact.getId();

        // Check there is a description unless it's a license that doesn't start with "license."
        // i.e. one that is specific to a given artifact
        if ((artifact.getDescription() == null) && (!isLicense(id) || id.getGroup().startsWith("license."))) {
            error("No description: " + editHint(id));
        }
    }

    private void verifyNameMatchesGroup(RepoArtifactId id) {
        // Check that the name matches the default name for the group
        // This is not an error, but is quite rare, so issue a warning
        if (!id.getName().equals(RepoArtifactId.defaultName(id.getGroup()))) {
            warn("Name mismatches group: " + id.toShortString());
        }
    }

    private void verifyStandardVersion(RepoArtifactId id) {
        if (!id.getVersion().isStandard()) {
            warn("Non-standard version: " + id.toShortString());
        }
    }

    private void verifyLicenses(RepoArtifact artifact) {
        RepoArtifactId id = artifact.getId();

        if ((artifact.getLicenses().size() == 0) && !isLicense(id) && !artifact.isStub() && 
                !artifact.getId().getType().equals("source")) {
            error("No license: " + editHint(id));
        }

        if (artifact.getLicenses().size() > 1) {
            warn("Multiple licenses: " + editHint(id));
        }

        // Check the license can be resolved
        for (Iterator i = artifact.getLicenses().iterator(); i.hasNext();) {
            RepoArtifactId licenseId = (RepoArtifactId)i.next();

            try {
                repository.resolve(licenseId, false);
            } catch (UnresolvedArtifactException e) {
                error("Unresolved artifact: " + licenseId.toShortString());
            }
        }
    }

    private boolean findMatchingPathSpec(Set pathSpecs, RepoPathSpec pathSpec) {
        for (Iterator i = pathSpecs.iterator(); i.hasNext();) {
            RepoPathSpec other = (RepoPathSpec)i.next();

            if (nsEquals(pathSpec.getFrom(), other.getFrom()) && nsEquals(pathSpec.getTo(), other.getTo())
                    && nsEquals(pathSpec.getOptions(), other.getOptions())
                    && nsEquals(pathSpec.isMandatory(), other.isMandatory())
                    && nsEquals(pathSpec.isDescend(), other.isDescend())) {
                return true;
            }
        }

        return false;
    }

    private boolean nsEquals(Object lhs, Object rhs) {
        return (lhs == null) ? (rhs == null) : lhs.equals(rhs);
    }

    private RepoDependency findMatchingDependency(RepoArtifact previous, RepoDependency dependency) {
        for (Iterator j = previous.getDependencies().iterator(); j.hasNext();) {
            RepoDependency previousDependency = (RepoDependency)j.next();

            if (toUnversioned(previousDependency.getId()).equals(toUnversioned(dependency.getId()))) {
                return previousDependency;
            }
        }

        return null;
    }

    /**
     * Makes life easier if using a FileRepository, but supplying the path of the file to edit
     */
    private String editHint(RepoArtifactId id) {
        String hint = "";

        if (repository.getClass().getName().equals("ws.quokka.core.repo_standard.FileRepository")) {
            File file = (File)new Reflect().invoke(repository, "getRepositoryFile", new Object[] { id });
            hint = "\t-> " + file.getPath();
        }

        return id.toShortString() + hint;
    }

    private void verifyUniqueMavenId(MavenId mavenId) {
        Set ids = (Set)mavenIds.get(mavenId);

        if (ids.size() != 1) {
            String message = "Duplicate maven ids: " + mavenId + ": " + ids;
            error(message);
        }
    }

    private void verifyChecksum(RepoArtifact artifact, String checksum) {
        Set ids = new TreeSet();

        for (Iterator i = artifacts.values().iterator(); i.hasNext();) {
            RepoArtifact other = (RepoArtifact)i.next();

            if (checksum.equals(other.getHash()) && !artifact.getId().equals(other.getId())) {
                ids.add(other.getId().toShortString());
            }
        }

        if (ids.size() != 0) {
            ids.add(artifact.getId().toShortString());

            String message = "Duplicate hashes: " + Strings.join(ids.iterator(), ", ");
            error(message);
        }
    }

    protected HashResult mavenHash(MavenId mavenId, TypedProperties properties, Project project) {
        String baseUrl = properties.getString("mavenUrl", "http://repo1.maven.org/maven2");
        baseUrl = baseUrl.endsWith("/") ? baseUrl : (baseUrl + "/");

        String user = properties.getString("mavenUser", null);
        String password = properties.getString("mavenPassword", null);
        File temp = null;
        URL url = null;

        try {
            temp = File.createTempFile("quokka-maven", ".md5");
            url = new URL(baseUrl + mavenId.getGroupId().replace('.', '/') + "/" + mavenId.getArtifactId() + "/"
                    + mavenId.getVersion() + "/" + mavenId.getArtifactId() + "-" + mavenId.getVersion() + "."
                    + mavenId.getType() + ".md5");

            new IOUtils().download(project, url, user, password, temp);

            String checksumString = new IOUtils().fileToString(temp).trim();
            Assert.isTrue(checksumString.length() >= 32, "Invalid hash stored at: " + url);

            return new HashResult(url.toString(), checksumString.substring(0, 32));
        } catch (Exception e) {
//            e.printStackTrace();
            error("Could not retrieve maven2 hash: " + mavenId + " from " + url + ": " + e.getMessage());

            return null;
        } finally {
            if (temp != null) {
                temp.delete();
            }
        }
    }

    protected HashResult hash(File file) {
        if (file == null) {
            return null;
        }

        Checksum checksum = (Checksum)new AntUtils(project).init(new Checksum(), "hash");
        checksum.setFile(file);

        String property = "q.temp.hash";
        PropertiesUtil.clearProperty(project, property); // Remove existing value as ANT will not overwrite

        checksum.setProperty(property);
        checksum.execute();

        return new HashResult(file.getPath(), project.getProperty(property));
    }

    //~ Inner Classes --------------------------------------------------------------------------------------------------

    public static class HashResult {
        private String source;
        private String hash;

        public HashResult(String source, String hash) {
            this.source = source;
            this.hash = hash;
        }

        public String getSource() {
            return source;
        }

        public String getHash() {
            return hash;
        }
    }
}
